﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Xml.Linq;
using System.Text.RegularExpressions;

namespace WindowsPhone.Marketplace
{
    public class WindowsPhoneMarketplace
    {
        /// <summary>
        /// Convenience array containing the known supported language codes. Feel free
        /// to pass in a code that is not in this list as it will just be passed to zest 
        /// directly.
        /// 
        /// Currently: http://msdn.microsoft.com/en-us/library/hh202918(v=vs.92).aspx
        /// </summary>
        public static string[] KnownLangCodes = new string[] 
        {  
            "zh-CN", "zh-TW", "cs-CZ", "da-DK", "nl-NL", "en-US", "en-GB", "fi-FI",
            "fr-FR", "de-DE", "el-GR", "hu-HU", "it-IT", "ja-JP", "ko-KR", "nb-NO",
            "pl-PL", "pt-BR", "pt-PT", "ru-RU", "es-ES", "sv-SE",
        };

        private string _xmlDir;
        
        /// <summary>
        /// </summary>
        /// <param name="xmlDir">The directory that contains all of the RSS dumps downloaded
        /// with the crawler</param>
        public WindowsPhoneMarketplace(string xmlDir)
        {
            if (!Directory.Exists(xmlDir))
                throw new ArgumentException("Provided xmlDir is not valid!");

            _xmlDir = xmlDir;
        }

        public Dictionary<string, XElement> GetXml(string[] langs)
        {
            Dictionary<string, XElement> xml = new Dictionary<string, XElement>();

            if (langs == null)
                langs = KnownLangCodes;

            foreach (string lang in langs)
            {
                xml.Add(lang, GetXml(lang));
            }

            return xml;
        }

        public XElement GetXml(string lang)
        {
            // TODO: add a way to get this string from the Crawler so that the format is only in one spot
            string file = Path.Combine(_xmlDir, "marketplace_") + lang + "_1" + ".xml";

            if (!File.Exists(file))
                throw new ArgumentException("lang (" + lang + ") is invalid. I couldn't find an XML file for it: " + file);

            using (StreamReader reader = new StreamReader(file))
            {
                string xml = reader.ReadToEnd();

                return XElement.Parse(xml);
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        public MarketplaceData GetMarketplaceData(string lang = null)
        {
            return GetMarketplaceData((lang == null ? null : new string[] { lang }));
        }

        public MarketplaceData GetMarketplaceData(string[] langs)
        {
            MarketplaceData data = new MarketplaceData();
            Dictionary<string, XElement> marketplaceXml = GetXml(langs);

            MarketplaceApp app;
            IEnumerable<XElement> entries;
            
            foreach (string lang in marketplaceXml.Keys)
            {
                entries = marketplaceXml[lang].Elements(MarketplaceSettings.AtomNamespace + "entry");

                foreach (XElement entry in entries)
                {
                    app = GetApp(data, lang, entry);

                    if (!data.Apps.ContainsKey(app.Id))
                    {
                        data.Apps.Add(app.Id, app);
                    }

                    // add this app to a specific marketplace
                    AddAppToMarketplace(data, lang, app);
                }
            }

            return data;
        }

        private void AddAppToMarketplace(MarketplaceData data, string lang, MarketplaceApp app)
        {
            MarketplaceData marketplace;

            if (!data.Marketplaces.ContainsKey(lang))
                data.Marketplaces[lang] = new MarketplaceLang(lang);

            marketplace = data.Marketplaces[lang];

            if (!marketplace.Apps.ContainsKey(app.Id))
                marketplace.Apps.Add(app.Id, app);

            // create a new publisher object per marketplace so that the data under the publisher
            // under that marketplace will only pertain to that particular publisher
            if (!marketplace.Publishers.ContainsKey(app.Publisher.Id))
                marketplace.Publishers.Add(app.Publisher.Id, new MarketplacePublisher(app.Publisher));

            if (!marketplace.Publishers[app.Publisher.Id].Apps.ContainsKey(app.Id))
                marketplace.Publishers[app.Publisher.Id].Apps.Add(app.Id, app);

            foreach (MarketplaceOffer offer in app.Offers[lang])
            {
                foreach (string paymentType in offer.PaymentType)
                {
                    if (!marketplace.PaymentTypes.ContainsKey(paymentType))
                        marketplace.PaymentTypes[paymentType] = new List<MarketplaceApp>();

                    marketplace.PaymentTypes[paymentType].Add(app);
                }
            }
        }

        /// <summary>
        /// Gets a MarketplaceApp object represented by the provided XML, by either 
        /// retrieveing an already created representation of this app or creates a new 
        /// MarketplaceApp entry.
        /// </summary>
        /// <param name="data"></param>
        /// <param name="entry"></param>
        /// <returns></returns>
        private MarketplaceApp GetApp(MarketplaceData data, string lang, XElement entry)
        {
            string id = Regex.Replace(entry.Element(MarketplaceSettings.AtomNamespace + "id").Value, "(urn:uuid:)(.)", "$2");

            MarketplaceApp app;

            if (data.Apps.ContainsKey(id))
            {
                app = data.Apps[id];
            }
            else
            {
                app = new MarketplaceApp();

                app.Title = entry.Element(MarketplaceSettings.AtomNamespace + "title").Value;
                app.Id = Regex.Replace(entry.Element(MarketplaceSettings.AtomNamespace + "id").Value, "(urn:uuid:)(.)", "$2");
                app.ReleaseDate = DateTime.Parse(entry.Element(MarketplaceSettings.ZestNamespace + "releaseDate").Value);
                app.Updated = DateTime.Parse(entry.Element(MarketplaceSettings.AtomNamespace + "updated").Value);
                app.ShortDescription = entry.Element(MarketplaceSettings.ZestNamespace + "shortDescription") == null ? "" : entry.Element(MarketplaceSettings.ZestNamespace + "shortDescription").Value;
                app.AverageUserRating = decimal.Parse(entry.Element(MarketplaceSettings.ZestNamespace + "averageUserRating").Value);
                app.UserRatingCount = int.Parse(entry.Element(MarketplaceSettings.ZestNamespace + "userRatingCount").Value);
                app.Version = entry.Element(MarketplaceSettings.ZestNamespace + "version").Value;
                app.ImageId = Regex.Replace(entry.Element(MarketplaceSettings.ZestNamespace + "image").Element(MarketplaceSettings.ZestNamespace + "id").Value, "(urn:uuid:)(.)", "$2");

                // turns out an app can have no categories listings
                XElement categoriesElement = entry.Element(MarketplaceSettings.ZestNamespace + "categories");
                
                if (categoriesElement != null)
                {
                    app.Categories = ParseCategories(data, app, entry.Element(MarketplaceSettings.ZestNamespace + "categories").Elements(MarketplaceSettings.ZestNamespace + "category"));
                }
                
                app.Publisher = ParsePublisher(data, app, entry.Element(MarketplaceSettings.ZestNamespace + "publisher"));
            }

            // add any new offers to this app (grouped by language)
            ParseOffers(data, lang, app, entry.Elements(MarketplaceSettings.ZestNamespace + "offers").Elements(MarketplaceSettings.ZestNamespace + "offer"));
            
            if (!app.Marketplaces.Contains(lang))
                app.Marketplaces.Add(lang);

            return app;
        }

        private void ParseOffers(MarketplaceData data, string lang, MarketplaceApp app, IEnumerable<XElement> offersXml)
        {

            MarketplaceOffer offer = new MarketplaceOffer();

            string paymentType;

            if (!app.Offers.ContainsKey(lang))
                app.Offers[lang] = new List<MarketplaceOffer>();

            foreach (XElement offerXml in offersXml)
            {
                offer.OfferId           = offerXml.Element(MarketplaceSettings.ZestNamespace + "offerId").Value;
                offer.MediaInstanceId   = offerXml.Element(MarketplaceSettings.ZestNamespace + "mediaInstanceId").Value;
                offer.Price             = decimal.Parse(offerXml.Element(MarketplaceSettings.ZestNamespace + "price").Value);
                offer.PriceCurrencyCode = offerXml.Element(MarketplaceSettings.ZestNamespace + "priceCurrencyCode").Value;
                offer.LicenseRight = offerXml.Element(MarketplaceSettings.ZestNamespace + "licenseRight").Value;
                
                if (!data.LicenseRights.Keys.Contains(offer.LicenseRight))
                    data.LicenseRights[offer.LicenseRight] = new List<MarketplaceApp>();

                data.LicenseRights[offer.LicenseRight].Add(app);

                foreach (XElement paymentTypeXml in offerXml.Elements(MarketplaceSettings.ZestNamespace + "paymentTypes").Elements(MarketplaceSettings.ZestNamespace + "paymentType"))
                {
                    paymentType = paymentTypeXml.Value;

                    if (!data.PaymentTypes.Keys.Contains(paymentType))
                    {
                        data.PaymentTypes[paymentType] = new List<MarketplaceApp>();
                    }

                    if (!data.PaymentTypes[paymentType].Contains(app))
                        data.PaymentTypes[paymentType].Add(app);

                    offer.PaymentType.Add(paymentType);
                }

                app.Offers[lang].Add(offer);
            }
        }

        private MarketplacePublisher ParsePublisher(MarketplaceData data, MarketplaceApp app, XElement publisherXml)
        {
            MarketplacePublisher publisher;

            string id = publisherXml.Element(MarketplaceSettings.ZestNamespace + "id").Value;

            if (data.Publishers.Keys.Contains(id))
            {
                publisher = data.Publishers[id];
            }
            else
            {
                publisher = new MarketplacePublisher();

                publisher.Id   = id;
                publisher.Name = publisherXml.Element(MarketplaceSettings.ZestNamespace + "name").Value;

                data.Publishers.Add(id, publisher);
            }

            if (!publisher.Apps.ContainsKey(app.Id))
                publisher.Apps.Add(app.Id, app);

            return publisher;
        }

        private IList<MarketplaceCategory> ParseCategories(MarketplaceData data, MarketplaceApp app, IEnumerable<XElement> categories)
        {
            MarketplaceCategory appCategory;
            string id;
            
            List<MarketplaceCategory> rv = new List<MarketplaceCategory>();

            foreach (XElement category in categories)
            {
                id = category.Element(MarketplaceSettings.ZestNamespace + "id").Value;

                // category already exists, just use the existing one
                if (data.Categories.Keys.Contains(id))
                {
                    appCategory = data.Categories[id];
                }
                else
                {
                    // new category, add it to the list
                    appCategory = new MarketplaceCategory();
                    appCategory.Id = id;
                    appCategory.Title = category.Element(MarketplaceSettings.ZestNamespace + "title").Value;
                    appCategory.IsRoot = category.Element(MarketplaceSettings.ZestNamespace + "isRoot").Value;

                    data.Categories[id] = appCategory;
                }

                rv.Add(appCategory);

                appCategory.Apps[app.Id] = app;
            }

            return rv;
        }
    }
}
